/* * * Panbox - encryption for cloud storage * Copyright (C) 2014-2015 by Fraunhofer SIT and Sirrix AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * Additonally, third party code may be provided with notices and open source * licenses from communities and third parties that govern the use of those * portions, and any licenses granted hereunder do not alter any rights and * obligations you may have under such open source licenses, however, the * disclaimer of warranty and limitation of liability provisions of the GPLv3 * will apply to all the product. * */ package org.panbox.core.crypto.io; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.Flushable; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Security; import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.io.FilenameUtils; import org.apache.log4j.Logger; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.panbox.PanboxConstants; import org.panbox.core.crypto.KeyConstants; import org.panbox.core.crypto.randomness.SecureRandomWrapper; import org.panbox.core.exception.FileEncryptionException; import org.panbox.core.exception.FileIntegrityException; import org.panbox.core.exception.RandomDataGenerationException; /** * @author palige * * Class offers transparent encryption/decryption for an encapsulated * {@link RandomAccessFile}-instance. Inheritance is intentionally * omitted in order to <br> * 1) omit exposure of any methods which may allow inadvertant direct * access to the ciphertext and <br> * 2) allow for custom exceptions <br> * This abstract class handles offset calculation, crypto metadata * management and offers multiple convenience methods replicating the * mode of operation of regular {@link RandomAccessFile}- and * {@link File}-instances. However, the actual work of encrypting and * decrypting the file contents is to be handled by the algorithm * specific implementations of this abstract class. */ public abstract class EncRandomAccessFile implements Flushable, Closeable { static { // add bouncycastle as default crypto provider Security.addProvider(new BouncyCastleProvider()); } private static final Logger log = Logger.getLogger("org.panbox.core"); /** * indicates if the underlying {@link RandomAccessFile} has been opened with * write access */ protected boolean writable; private boolean open; /** * @return <code>true</code>, if this file currently is opened, * <code>false</code> otherwise */ public synchronized boolean isOpen() { return open; } /** * set open parameter */ protected synchronized void setOpen(boolean open) { this.open = open; } /** * stores the {@link RandomAccessFile}-instance being used for reading * writing the actual encrypted data */ protected RandomAccessFile backingRandomAccessFile; /** * stores a {@link File}-instance of the backend-file */ protected File backingFile; /** * header access layer for this {@link EncRandomAccessFile}-instance */ protected FileHeader fHeader; /** * indicates if an implementation of this abstract class implements * integrity checking (or an AE mode) */ abstract boolean implementsAuthentication(); /** * indicates if an implementation of this class uses the read()/write() * caches */ abstract boolean implementsCaching(); /** * stores the base multiple for chunk size calculation. One chunk comprises * the given number of blocks */ protected static final int CHUNK_MULTIPLE = 4096; // 1024; protected int CHUNK_IV_SIZE; protected int CHUNK_DATA_SIZE; public int getVirtualChunkSize() { return CHUNK_DATA_SIZE; } protected int CHUNK_SIZE; /** * corresponding length values of encrypted chunks (GCM needs additional * space for authentication metadata) */ protected int CHUNK_ENC_DATA_SIZE; protected int CHUNK_ENC_SIZE; // public String CIPHER_CHUNK; protected int CHUNK_TLEN; /** * stores the {@link SecretKey}-instance to be used for de-/encryption of * the file key */ protected SecretKey shareKey; /** * to be set by implementations */ public int BLOCK_LENGTH; /** * @return name of algorithm used by implementations */ abstract String getAlgorithmIdentifier(); /** * @return the crypto provider an implementation uses */ abstract String getCryptoProvider(); final int SHARE_KEY_SIZE = KeyConstants.SYMMETRIC_KEY_SIZE_BYTES; /** * method sets this instances share key and completes initialization * * @param shareKey * the share key for this file * @throws IOException * @throws FileIntegrityException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidKeyException * @throws NoSuchProviderException * @throws NoSuchPaddingException * @throws NoSuchAlgorithmException */ public synchronized void initWithShareKey(SecretKey shareKey) throws FileEncryptionException, IOException, FileIntegrityException { if (!isOpen()) { throw new IOException("File has not been opened!"); } else { if (shareKey == null) { throw new FileEncryptionException("Secret key is null!"); } else if (!isInitialized()) { this.shareKey = shareKey; try { readMetadata(); setInitialized(true); } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException e) { this.shareKey = null; throw new FileEncryptionException( "Error reading metadata!", e); } } else { log.debug("initWithShareKey(): instance has already been initialized with share key!"); } } } /** * indicates if {@link #initWithShareKey(SecretKey)} has been called for * this instance */ private boolean initialized = false; public boolean isInitialized() { return initialized; } protected void setInitialized(boolean initialized) { this.initialized = initialized; } /** * stores {@link Cipher}-instance for decryption */ protected Cipher decCipher; /** * stores {@link Cipher}-instance for encryption */ protected Cipher encCipher; /** * cache for temporary storage of chunks */ protected ChunkCache cache; /** * stores an instance of the management class for verification of * authentication tag integrity */ private AuthTagVerifier authTagVerifier; protected AuthTagVerifier getAuthTagVerifier() { return authTagVerifier; } protected void setAuthTagVerifier(AuthTagVerifier authTagVerifier) throws FileEncryptionException { if (this.authTagVerifier == null) { this.authTagVerifier = authTagVerifier; } else { throw new FileEncryptionException( "instance authetication tag verifier has already been set!"); } } /** * base constructor, instantiation work for actual backend files is handled * by {@link #create(int, SecretKey)} and {@link #open()} methods * * @throws RandomDataGenerationException * @throws NoSuchProviderException * @throws InvalidAlgorithmParameterException * @throws NoSuchPaddingException * @throws NoSuchAlgorithmException * @throws InvalidKeyException * @throws IOException * */ protected EncRandomAccessFile(File backingFile) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException, RandomDataGenerationException, IOException { if (backingFile.isDirectory()) throw new IOException("Instantiation failed. " + backingFile + " is a directory!"); initCiphers(); initParams(); this.backingFile = backingFile; } /** * initialization of implementation specific parameters */ abstract void initParams(); /** * Method reads crypto meta data from the file header and should further, if * the implementation supports integrity protection, perform initialization * of the authentication tag tree structure for later checks of * authentication tag integrity. * * @throws FileEncryptionException * @throws IOException * @throws InvalidKeyException * @throws IllegalBlockSizeException * @throws BadPaddingException * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws NoSuchProviderException * @throws FileIntegrityException */ abstract protected void readMetadata() throws FileEncryptionException, IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, FileIntegrityException; /** * Method handles share key updates by creating a new pseudo random file * encryption key for this file, re-encrypting the files contents,then * re-encrypting new file encryption key with the given share key and * storing it within the file header alongside its corresponding version * number. * * @param shareKeyVersion * version number of the new share key * @param shareKey * the new share key */ public synchronized void reencryptFile(int shareKeyVersion, SecretKey shareKey) { // NOTE: currently no implementation and no caller } /** * retrieves the root authentication tag over all single chunk * authentication tags stored within the file * * @return the root authentication tag stored within the header of this * file, if integrity checking is implemented, <code>null</code> * otherwise */ abstract protected byte[] readFileAuthenticationTag(); /** * writes the root authentication tag over all single chunk authentication * tags to the header * * @param rootAuthTag */ abstract protected void writeFileAuthenticationTag(byte[] rootAuthTag) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, FileEncryptionException, IOException; /** * Method indicates if chunk authentication tags currently stored on disk * are valid w.r.t. the central file authentication tag being stored in the * file header. Prior to the verification, any chunk data currently being * stored in the cache will be written to disk and the auth tag tree will be * re-built. * * @return <code>true</code>, if file chunk authentication tags are valid, * <code>false</code> otherwise * @throws IOException * @throws RandomDataGenerationException * @throws FileEncryptionException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException */ abstract public boolean checkFileAuthenticationTag() throws FileEncryptionException, IOException; /** * initialize ciphers * * @param key * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws InvalidKeyException * @throws RandomDataGenerationException * @throws InvalidAlgorithmParameterException * @throws NoSuchProviderException */ abstract protected void initCiphers() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, RandomDataGenerationException, InvalidAlgorithmParameterException, NoSuchProviderException; /** * {@link SecureRandomWrapper} instance for IV generation */ protected SecureRandomWrapper srWrapper; /** * generates a random initialization vector for chunk encryption * * @return new random chunk IV of size {@link Cipher#getBlockSize()} * @throws FileEncryptionException * @throws RandomDataGenerationException */ protected byte[] generateRandomChunkIV() throws FileEncryptionException, RandomDataGenerationException { byte[] res = new byte[BLOCK_LENGTH]; srWrapper.nextBytes(res); // just to be sure if ((res != null) && (res.length == BLOCK_LENGTH)) { return res; } else { throw new FileEncryptionException("Chunk IV generation failed!"); } } /** * Inner class for caching a single chunk for reading and writing, * respectively. NOTE: Cached chunks are stored in plain text until they are * actually to be written to disk. Correspondingly, no * authentication/integrity checking is done when a cached chunk is being * read. */ protected class ChunkCache { // /** // * stores the index of the chunk which has been cached before the // chunk // * being currently cached, or -1 if no chunk had been cached before // */ // protected long previousChunkIdx = -1; /** * stores the index of the chunk currently being cached */ protected long chunkIdx; /** * holds the actual chunk */ protected byte[] chunkBuffer; /** * indicates if the chunk currently being held in this cache still needs * to be written to disk (i.e., has been set from within a write* call) */ protected boolean needsToBeWritten; /** * indicates if chunk currently being held in cache is a last chunk */ protected boolean isLast; protected synchronized void setChunkBuffer(long idx, byte[] chunk, boolean needsdToBeWritten, boolean isLast) { this.chunkIdx = idx; // only store a *copy* of this array to avoid modification of its // contents due to stale pointers this.chunkBuffer = Arrays.copyOf(chunk, chunk.length); this.needsToBeWritten = needsdToBeWritten; this.isLast = isLast; // this.previousChunkIdx = previousChunkIdx; } protected synchronized byte[] getChunkBuffer(long index) { return ((this.chunkIdx == index) && (this.chunkBuffer != null)) ? Arrays .copyOf(chunkBuffer, chunkBuffer.length) : null; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { StringBuffer res = new StringBuffer(); res.append("ChunkCache:"); res.append("index=" + this.chunkIdx + ";"); res.append("length=" + this.chunkBuffer.length + ";"); res.append("needsToBeWritten=" + this.needsToBeWritten + ";"); res.append("isLast=" + this.isLast); return res.toString(); } } /** * convenience class for long to byte[] conversion (and vice-versa) * * (see <a href= * "https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to-byte-and-back-in-java" * >https://stackoverflow.com/questions/4485128/how-do-i-convert-long-to- * byte-and-back-in-java</a> */ protected static class LongByteConv { private static ByteBuffer buf = ByteBuffer.allocate(Long.SIZE / 8); public static byte[] long2Bytes(long x) { buf.clear(); buf.putLong(0, x); return buf.array(); } public static long bytes2Long(byte[] bytes) { buf.clear(); buf.put(bytes, 0, bytes.length); buf.flip();// need flip return buf.getLong(); } } /** * inner class for accessing header information stored within this * {@link EncRandomAccessFile}-instance */ protected class FileHeader { /** * we may use AES in ECB mode for file key encryption, as * sizeof(file_key) == sizeof(share_key) */ private final String CIPHER_FILEKEY = "AES/ECB/NoPadding"; private Cipher filekeyCipher; /** * magic number, 6 bytes */ final byte[] PANBOX_FILE_MAGIC = PanboxConstants.PANBOX_FILE_MAGIC; /** * version field, 4 bytes */ final byte[] PANBOX_FILE_VERSION = PanboxConstants.PANBOX_VERSION; /** * decrypted file key */ private SecretKey decryptedFileKey; /** * stores the version number of the share key */ private int shareKeyVersion = -1; /** * stored file authentication tag */ private byte[] fileAuthTag; /** * stored header authentication tag */ private byte[] headerAuthTag; /** * stores this headers size, currently 6+4+4+32+32 = 78 bytes without * authentication, or 6+4+4+32+32+32 = 110 bytes *with* authentication */ private final int HEADER_SIZE; /** * hard coded auth tag size for SHA256Digest */ final static int AUTH_TAG_SIZE = 32; /** * HMAC for creating the header authentication tag */ private HMac headerAuthHMac; protected final FieldOffsets OffsetTable; protected FileHeader() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { this.headerAuthHMac = new HMac(new SHA256Digest()); this.headerAuthTag = new byte[AUTH_TAG_SIZE]; this.filekeyCipher = Cipher.getInstance(CIPHER_FILEKEY, "BC"); if (implementsAuthentication()) { // include file auth tag // this.fileAuthTag = new byte[AuthTagVerifier.AUTH_TAG_SIZE]; this.HEADER_SIZE = PANBOX_FILE_MAGIC.length + PANBOX_FILE_VERSION.length + AuthTagVerifier.AUTH_TAG_SIZE + FileHeader.AUTH_TAG_SIZE // header auth tag + KeyConstants.SYMMETRIC_FILE_KEY_SIZE_BYTES // encrypted // file // key + (Integer.SIZE / 8); // shareKeyVersion } else { this.HEADER_SIZE = PANBOX_FILE_MAGIC.length + PANBOX_FILE_VERSION.length + FileHeader.AUTH_TAG_SIZE + KeyConstants.SYMMETRIC_FILE_KEY_SIZE_BYTES // encrypted // file // key + (Integer.SIZE / 8); // shareKeyVersion } this.OffsetTable = new FieldOffsets(); } /** * byte offsets of header fields */ class FieldOffsets { // offset_n := offset_(n-1) + sizeof(field@offset_(n-1)) protected final int FIELD_PANBOX_FILE_MAGIC = 0; protected final int FIELD_PANBOX_FILE_VERSION = FIELD_PANBOX_FILE_MAGIC + PANBOX_FILE_MAGIC.length; protected final int FIELD_SHARE_KEY_VERSION = FIELD_PANBOX_FILE_VERSION + PANBOX_FILE_VERSION.length; protected final int FIELD_FILE_KEY = FIELD_SHARE_KEY_VERSION + (Integer.SIZE / 8); protected final int FIELD_FILE_AUTH_TAG = FIELD_FILE_KEY + KeyConstants.SYMMETRIC_FILE_KEY_SIZE_BYTES;; protected final int FIELD_HEADER_AUTH_TAG = implementsAuthentication() ? FIELD_FILE_AUTH_TAG + AuthTagVerifier.AUTH_TAG_SIZE : FIELD_FILE_AUTH_TAG; } /** * writes header data to the file * * @throws FileEncryptionException * @throws IOException * @throws InvalidKeyException * @throws BadPaddingException * @throws IllegalBlockSizeException */ protected synchronized void write() throws FileEncryptionException, IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { // check if all necessary data has been set if ((getDecryptedFileKey() == null) || (getDecryptedFileKey().getEncoded().length == 0)) { throw new FileEncryptionException( "Decrypted file key has not been set!"); } // if (implementsAuthentication() // && ((getFileAuthTag() == null) || (getFileAuthTag().length == // 0))) { // throw new FileEncryptionException( // "File authentication tag has not been set"); // } if (getShareKeyVersion() < 0) { throw new FileEncryptionException( "Share key version number has not been set!"); } // if all data have been set, initialize HMac with shareKey if (shareKey == null || shareKey.getEncoded().length == 0) { throw new FileEncryptionException( "Invalid share key in encrypting random access file!"); } else { headerAuthHMac.reset(); KeyParameter keyParameter = new KeyParameter( shareKey.getEncoded()); headerAuthHMac.init(keyParameter); } // encrypt file key byte[] tmpencryptedFileKey = new byte[KeyConstants.SYMMETRIC_FILE_KEY_SIZE_BYTES]; filekeyCipher.init(Cipher.ENCRYPT_MODE, shareKey); byte[] t2 = decryptedFileKey.getEncoded(); if ((t2 == null) || (t2.length != KeyConstants.SYMMETRIC_FILE_KEY_SIZE_BYTES)) { throw new FileEncryptionException( "Encoded file key null or invalid length!"); } tmpencryptedFileKey = filekeyCipher.doFinal(t2); // create output buffer & write header data ByteArrayOutputStream bstream = new ByteArrayOutputStream(); DataOutputStream ostream = new DataOutputStream(bstream); ostream.write(PANBOX_FILE_MAGIC); ostream.write(PANBOX_FILE_VERSION); ostream.writeInt(shareKeyVersion); ostream.write(tmpencryptedFileKey); if (implementsAuthentication()) { if (getFileAuthTag() == null) { // if no chunks have been stored yet, the initial file auth // tag will be set to zero byte[] emptyAuthTag = new byte[AuthTagVerifier.AUTH_TAG_SIZE]; Arrays.fill(emptyAuthTag, (byte) 0x00); ostream.write(emptyAuthTag); // setFileAuthTag(null); } else { ostream.write(fileAuthTag); } } ostream.close(); // all data have been written to stream, get array byte[] header_data = bstream.toByteArray(); // calculate hmac headerAuthHMac.update(header_data, 0, header_data.length); headerAuthHMac.doFinal(headerAuthTag, 0); // write data and hmac long oldpos = backingRandomAccessFile.getFilePointer(); backingRandomAccessFile.seek(0); backingRandomAccessFile.write(header_data); backingRandomAccessFile.write(headerAuthTag); backingRandomAccessFile.seek(oldpos); } /** * reads magic + share key version without verification * * @throws FileEncryptionException * @throws IOException */ protected synchronized void readDontVerify() throws IOException, FileEncryptionException { // check file length if (backingRandomAccessFile.length() < headerSize()) { throw new FileEncryptionException("Invalid file header"); } long oldpos = backingRandomAccessFile.getFilePointer(); backingRandomAccessFile.seek(0); byte[] header_data = new byte[headerSize() - FileHeader.AUTH_TAG_SIZE]; backingRandomAccessFile.read(header_data); backingRandomAccessFile.seek(oldpos); DataInputStream istream = new DataInputStream( new ByteArrayInputStream(header_data)); // check magic number byte[] tmpmagic = new byte[PANBOX_FILE_MAGIC.length]; istream.read(tmpmagic); if (!Arrays.equals(tmpmagic, PANBOX_FILE_MAGIC)) { throw new FileEncryptionException( "Invalid magic number in file header"); } // check version field byte[] tmpversion = new byte[PANBOX_FILE_VERSION.length]; istream.read(tmpversion); if (!Arrays.equals(tmpversion, PANBOX_FILE_VERSION)) { throw new FileEncryptionException( "Invalid version in file header. Expected version is " + PANBOX_FILE_VERSION.toString()); } // if we got here, read non-final fields // share key version this.shareKeyVersion = istream.readInt(); istream.close(); } /** * reads and verifies all header data from the file * * @throws InvalidKeyException * @throws IOException * @throws FileEncryptionException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws FileIntegrityException */ protected synchronized void readAndVerify() throws InvalidKeyException, IOException, FileEncryptionException, IllegalBlockSizeException, BadPaddingException, FileIntegrityException { // check file length if (backingRandomAccessFile.length() < headerSize()) { throw new FileEncryptionException("Invalid file header"); } // initialize HMac with shareKey if (shareKey == null || shareKey.getEncoded().length == 0) { throw new FileEncryptionException( "Invalid share key in encrypting random access file!"); } else { headerAuthHMac.reset(); KeyParameter keyParameter = new KeyParameter( shareKey.getEncoded()); headerAuthHMac.init(keyParameter); } long oldpos = backingRandomAccessFile.getFilePointer(); backingRandomAccessFile.seek(0); byte[] header_data = new byte[headerSize() - FileHeader.AUTH_TAG_SIZE]; backingRandomAccessFile.read(header_data); // read stored value of header authentication tag backingRandomAccessFile.read(headerAuthTag); backingRandomAccessFile.seek(oldpos); // update hmac for header auth tag verification headerAuthHMac.update(header_data, 0, header_data.length); byte[] hmacRef = new byte[AUTH_TAG_SIZE]; headerAuthHMac.doFinal(hmacRef, 0); if (!Arrays.equals(hmacRef, headerAuthTag)) { throw new FileIntegrityException( "HMac of file header is invalid!"); } else { DataInputStream istream = new DataInputStream( new ByteArrayInputStream(header_data)); // check magic number byte[] tmpmagic = new byte[PANBOX_FILE_MAGIC.length]; istream.read(tmpmagic); if (!Arrays.equals(tmpmagic, PANBOX_FILE_MAGIC)) { throw new FileEncryptionException( "Invalid magic number in file header"); } // check version field byte[] tmpversion = new byte[PANBOX_FILE_VERSION.length]; istream.read(tmpversion); if (!Arrays.equals(tmpversion, PANBOX_FILE_VERSION)) { throw new FileEncryptionException( "Invalid version in file header. Expected version is " + PANBOX_FILE_VERSION.toString()); } // if we got here, read non-final fields // share key version this.shareKeyVersion = istream.readInt(); // encrypted file key byte[] tmpencryptedFileKey = new byte[KeyConstants.SYMMETRIC_FILE_KEY_SIZE_BYTES]; istream.read(tmpencryptedFileKey); filekeyCipher.init(Cipher.DECRYPT_MODE, shareKey); this.decryptedFileKey = new SecretKeySpec( filekeyCipher.doFinal(tmpencryptedFileKey), getAlgorithmIdentifier()); // file auth tag if (implementsAuthentication()) { byte[] tmpFileAuthBuf = new byte[AuthTagVerifier.AUTH_TAG_SIZE]; istream.read(tmpFileAuthBuf); // if an empty file auth tag has been stored (i.e., no // chunks have been stored so far), set the field value to // null byte[] zeroBuf = new byte[AuthTagVerifier.AUTH_TAG_SIZE]; Arrays.fill(zeroBuf, (byte) 0x00); if (Arrays.equals(zeroBuf, tmpFileAuthBuf)) { setFileAuthTag(null); } else { setFileAuthTag(tmpFileAuthBuf); } } istream.close(); } } /** * @return the size of this header in bytes */ protected int headerSize() { return this.HEADER_SIZE; } protected SecretKey getDecryptedFileKey() { return decryptedFileKey; } protected void setDecryptedFileKey(SecretKey decryptedFileKey) { this.decryptedFileKey = decryptedFileKey; } protected int getShareKeyVersion() { return shareKeyVersion; } protected void setShareKeyVersion(int shareKeyVersion) { this.shareKeyVersion = shareKeyVersion; } protected byte[] getFileAuthTag() { return fileAuthTag; } protected void setFileAuthTag(byte[] fileAuthTag) { this.fileAuthTag = fileAuthTag; } } /** * basic helper class converting byte to boolean values and vice versa */ protected static class BooleanByteConv { public static boolean byte2bool(byte b) { return ((b & 0x01) != 0); } public static byte[] bool2byte(boolean b) { return b ? new byte[] { 1 } : new byte[] { 0 }; } } /** * helper class for converting ints to their respective * byte[]-representation and vice versa */ protected static class IntByteConv { public static byte[] int2byte(int arg) { byte[] ret = new byte[4]; ret[0] = (byte) (arg >> 24); ret[1] = (byte) (arg >> 16); ret[2] = (byte) (arg >> 8); ret[3] = (byte) (arg >> 0); return ret; } public static int byte2int(byte[] arg) { return (arg[0] << 24) & 0xff000000 | (arg[1] << 16) & 0xff0000 | (arg[2] << 8) & 0xff00 | (arg[3] << 0) & 0xff; } } protected SecretKey getFileKey() { return fHeader.getDecryptedFileKey(); } /** * Method for reading and decrypting a chunk within the file. To be * implemented by the algorithm-specific implementations. * * @param buffer * @param index * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws FileEncryptionException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws IOException * @throws FileIntegrityException * @throws ShortBufferException */ abstract protected byte[] _readChunk(long index) throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, FileEncryptionException, IllegalBlockSizeException, BadPaddingException, FileIntegrityException; /** * Method for reading and decrypting the last chunk of the file. To be * implemented by the algorithm-specific implementations. * * @param buffer * @param index * @throws IOException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws FileEncryptionException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws ShortBufferException * @throws FileIntegrityException */ abstract protected byte[] _readLastChunk(long index) throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, FileEncryptionException, ShortBufferException, FileIntegrityException; /** * Method for encrypting and writing a chunk within the file. To be * implemented by the algorithm-specific implementations. * * @param buffer * @param index * @throws RandomDataGenerationException * @throws IOException * @throws FileEncryptionException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws ShortBufferException * @throws Exception */ abstract protected void _writeChunk(byte[] buffer, long index) throws FileEncryptionException, RandomDataGenerationException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException; /** * Method for encrypting and writing the last chunk of the file. To be * implemented by the algorithm-specific implementations. * * @param buffer * @param index * @throws RandomDataGenerationException * @throws FileEncryptionException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws IOException * @throws ShortBufferException */ abstract protected void _writeLastChunk(byte[] buffer, long index) throws FileEncryptionException, RandomDataGenerationException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException; protected synchronized byte[] readChunk(long index) throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, FileEncryptionException, ShortBufferException, RandomDataGenerationException, FileIntegrityException { if (implementsCaching()) { // check cache byte[] tmpChunk = cache.getChunkBuffer(index); if (tmpChunk != null) { return tmpChunk; } else { flush(false); tmpChunk = _readChunk(index); cache.setChunkBuffer(index, tmpChunk, false, false); return tmpChunk; } } else { return _readChunk(index); } } protected synchronized byte[] readLastChunk(long index) throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, FileEncryptionException, ShortBufferException, RandomDataGenerationException, FileIntegrityException { if (implementsCaching()) { // check cache byte[] tmpChunk = cache.getChunkBuffer(index); if (tmpChunk != null) { return tmpChunk; } else { flush(false); tmpChunk = _readLastChunk(index); cache.setChunkBuffer(index, tmpChunk, false, true); return tmpChunk; } } else { return _readLastChunk(index); } } protected synchronized void flush(boolean flushauthdata) throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, FileEncryptionException, RandomDataGenerationException, IOException { if (writable && implementsCaching()) { if (cache.needsToBeWritten) { if (!cache.isLast) { _writeChunk(cache.chunkBuffer, cache.chunkIdx); } else { _writeLastChunk(cache.chunkBuffer, cache.chunkIdx); } cache.needsToBeWritten = false; } if (flushauthdata) { flushAuthData(); } } } @Override public synchronized void flush() throws IOException { try { if (implementsAuthentication()) { flush(true); } else { flush(false); } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | FileEncryptionException | RandomDataGenerationException e) { throw new IOException(e.getMessage()); } } protected synchronized void writeChunk(byte[] buffer, long index) throws FileEncryptionException, RandomDataGenerationException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { if (implementsCaching()) { // if we use caching mode, only write chunk in case it hasn't been // written before & if we're about to write a new chunk (with a new // index) if (index != cache.chunkIdx) { flush(false); } if (buffer.length == 0) { log.warn("writeLastChunk(): buffer has length 0!"); return; } else { cache.setChunkBuffer(index, buffer, true, false); // System.err.println("writeChunk(" + index + ") - cached " + // cache); adjustBackingFileLength(buffer, index); } } else { if (buffer.length == 0) { log.warn("writeLastChunk(): buffer has length 0!"); return; } else { _writeChunk(buffer, index); } } } protected synchronized void writeLastChunk(byte[] buffer, long index) throws FileEncryptionException, RandomDataGenerationException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { if (implementsCaching()) { // if we use caching mode, only write chunk in case it hasn't been // written before & if we're about to write a new chunk (with a new // index) if (index != cache.chunkIdx) { flush(false); } if (buffer.length == 0) { log.warn("writeLastChunk(): buffer has length 0!"); return; } else { cache.setChunkBuffer(index, buffer, true, true); // System.err.println("writeChunk(" + index + ") - cached " + // cache); adjustBackingFileLength(buffer, index); } } else { if (buffer.length == 0) { log.warn("writeLastChunk(): buffer has length 0!"); return; } else { _writeLastChunk(buffer, index); } } } /** * Helper methods which adjusts the actual length of the backing file for * caching. As with caching, data is only actually written to disk, if a * chunk has been filled completely and a new chunk is being accessed, the * file size has to be extended in advance in order for methods like * seek/skipBytes/... to still be able to work. * * @param buffer * @param index * @throws IOException */ private void adjustBackingFileLength(byte[] buffer, long index) throws IOException { if (buffer == null || buffer.length == 0) { throw new IOException("Buffer null or empty!"); } // as data is only written when a chunk has been filled completely, // backing file size has to be adapted, so skipBytes etc. still work long newsize = fHeader.headerSize() + (index * CHUNK_ENC_SIZE) + CHUNK_IV_SIZE + buffer.length + CHUNK_TLEN; if (newsize > realLength()) { // only change size if it has been increased backingRandomAccessFile.setLength(newsize); // System.err.println("writeChunk(" + index + ") - setLen(" + // newsize + ")"); } } public synchronized int read() throws IOException, FileEncryptionException, FileIntegrityException { // if we read beyond the EOF, always return -1 if (getFilePointer() >= length()) return -1; try { // check associated chunk index int offset = (int) currentchunkoffset(); byte[] decChunk = null; // check if we read last chunk if (currentchunkpointer() == lastchunkpointer()) { decChunk = readLastChunk(currentchunkpointer()); } else { decChunk = readChunk(currentchunkpointer()); } skipBytes(1); if (offset >= decChunk.length) { throw new FileEncryptionException( "read() offset mismatch in decrypted data!"); } else { // mask resulting byte return (int) (decChunk[offset] & 0xff); } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | ShortBufferException | RandomDataGenerationException e) { throw new FileEncryptionException("Decryption error during read()", e); } } /** * current recursion level in {@link #read(byte[], int, int)} */ private int readRecursionProtector = -1; public synchronized int read(byte[] b, int off, int len) throws IOException, FileEncryptionException, FileIntegrityException { if (readRecursionProtector > 1) { throw new FileEncryptionException("read recursion level exceeded!"); } else { readRecursionProtector++; } try { // if we read beyond the EOF, always return -1 if (getFilePointer() >= length()) { if (readRecursionProtector > 0) { log.error("read(): recursive read() at end of file " + backingFile.getName() + "!"); } readRecursionProtector--; return -1; } if ((b == null) || ((b.length - off) < len)) throw new IOException("buffer null or empty"); // check preconditions int offset = (int) currentchunkoffset(); boolean lastchunk, singlechunk; // true if we initially read within the last chunk lastchunk = (currentchunkpointer() == lastchunkpointer()); // true if we have to read more than one chunk singlechunk = (len <= (CHUNK_DATA_SIZE - offset)) || lastchunk; byte[] chunk; // = new byte[CHUNK_SIZE]; if (singlechunk) { // we keep in one chunk // check if we read last chunk chunk = lastchunk ? readLastChunk(currentchunkpointer()) : readChunk(currentchunkpointer()); // number of bytes to copy is the minimum of the following // values: // - length of bytes to write to given array // - length of chunk returned by readLastChunk minus read offset // within the last chunk int n = Math.min(chunk.length - offset, len); // copy current decrypted chunk to corresponding location in // target array System.arraycopy(chunk, offset, b, off, n); // advance file pointer skipBytes(n); readRecursionProtector--; return n; } else { // we need to read more than one chunk // strategy: split array into preceding bytes, a number of // chunk-sized pieces and remaining bytes. read these parts // separately and recursively; NOTE: max recursion level should // be 1 int ret = 0; // calculate partial chunk at beginning int preceding = 0; if (offset != 0) { preceding = CHUNK_DATA_SIZE - offset; if (preceding > 0) { ret += read(b, off, preceding); if (ret < preceding) { readRecursionProtector--; return ret; } } } // we are now aligned with the current chunk offset // calculate number of chunks to be read int nchunks = (len - preceding) / CHUNK_DATA_SIZE; // read chunk-sized pieces for (int i = 0; i < nchunks; i++) { ret += read(b, off + preceding + (i * CHUNK_DATA_SIZE), CHUNK_DATA_SIZE); if (ret < (preceding + ((i + 1) * CHUNK_DATA_SIZE))) { readRecursionProtector--; return ret; } } // remaining bytes int remainder = (len - preceding) % CHUNK_DATA_SIZE; // read remaining bytes, if there are any if (remainder > 0) { ret += read(b, off + preceding + (nchunks * CHUNK_DATA_SIZE), remainder); if (ret < (preceding + (nchunks * CHUNK_DATA_SIZE) + remainder)) { readRecursionProtector--; return ret; } } readRecursionProtector--; return ret; } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | ShortBufferException | RandomDataGenerationException e) { throw new FileEncryptionException("Error during read()", e); } } public synchronized int read(byte[] b) throws IOException, FileEncryptionException, FileIntegrityException { return this.read(b, 0, b.length); } /** * Encrypts and writes a single byte to the file. If this file does not yet * exist, {@link #createNewFile()} will be called upon the first write call. * * @see java.io.RandomAccessFile#write(int) * * @param b * @throws IOException * @throws FileEncryptionException * @throws FileIntegrityException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws ShortBufferException * @throws RandomDataGenerationException */ public synchronized void write(int b) throws IOException, FileEncryptionException, FileIntegrityException { if (!exists() || realLength() == 0) { throw new FileEncryptionException( "File has not been initialized properly!"); } try { // offset within chunk long curpos = getFilePointer(); // initial write call at beginning of the file needs extra care as // we // have no preceding chunks AND are in the last chunk at the same // time if ((length() == 0) && (curpos == 0)) { byte[] lastchunk = new byte[1]; lastchunk[0] = (byte) b; writeLastChunk(lastchunk, 0); skipBytes(1); return; } int offset = currentchunkoffset(); // first decrypt chunk, then write byte to plaintext, then // re-encrypt byte[] decChunk; // check where exactly we're writing to long curchunk = currentchunkpointer(); if (curchunk < lastchunkpointer()) { // we're writing within one of the regular chunks decChunk = readChunk(currentchunkpointer()); if (offset >= decChunk.length) { throw new FileEncryptionException( "read() offset mismatch in decrypted data!"); } // set byte and write chunk decChunk[offset] = (byte) b; writeChunk(decChunk, currentchunkpointer()); skipBytes(1); } else if (curchunk == lastchunkpointer()) { // we're starting to write within the current last chunk of this // file; we are NOT just about to start a new last chunk decChunk = readLastChunk(currentchunkpointer()); if (offset >= decChunk.length) { // we write beyond the former last byte in the last chunk byte[] lastChunk = new byte[offset + 1]; // copy old chunk System.arraycopy(decChunk, 0, lastChunk, 0, decChunk.length); Arrays.fill(lastChunk, decChunk.length, offset, (byte) 0x00); lastChunk[offset] = (byte) b; writeLastChunk(lastChunk, currentchunkpointer()); skipBytes(1); } else { // simply set byte and write chunk decChunk[offset] = (byte) b; writeLastChunk(decChunk, currentchunkpointer()); skipBytes(1); } } else { // we're writing beyond the former last chunk of this file. this // may // happen // 1. by seeking beyond the former length of this file, // 2. when just having completely filled the former last chunk // strategy: // 1. identify index of new last chunk // 2. CBC-reencrypt all chunks within // [index_oldlastchunk;index_new // astchunk-1] // 3. write new last chunk with given byte at given offset // note: data in between the files old length and the given new // offset to write to are undefined. in this case, we're going // to // write null bytes long idx_newlastchunk = curchunk; long idx_oldlastchunk = lastchunkpointer(); // number of chunks between old and new last chunk, e.g. in case // of seek() long diff = idx_newlastchunk - idx_oldlastchunk - 1; // reencrypt old last chunk decChunk = readLastChunk(idx_oldlastchunk); byte[] chunk = new byte[CHUNK_DATA_SIZE]; System.arraycopy(decChunk, 0, chunk, 0, decChunk.length); // fill remaining space with null bytes Arrays.fill(chunk, decChunk.length, CHUNK_DATA_SIZE, (byte) 0x00); writeChunk(chunk, idx_oldlastchunk); // write intermediate chunks, if there are any (e.g. seek into // file) for (long i = diff; i > 0; i--) { Arrays.fill(chunk, (byte) 0x00); writeChunk(chunk, idx_newlastchunk - i); skipBytes(CHUNK_DATA_SIZE); } // now write new last chunk byte[] lastchunk = new byte[offset + 1]; Arrays.fill(lastchunk, (byte) 0x00); lastchunk[offset] = (byte) b; writeLastChunk(lastchunk, idx_newlastchunk); skipBytes(1); } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | ShortBufferException | RandomDataGenerationException e) { throw new FileEncryptionException("Error during read()", e); } } public synchronized void write(byte[] b) throws IOException, FileEncryptionException, FileIntegrityException { this.write(b, 0, b.length); } /** * current recursion level in {@link #write(byte[], int, int)} */ private int writeRecursionProtector = -1; /** * Encrypts and writes the given byte array to the file. If the number of * bytes to write exceed the chunk-size, the array in split and recursively * written in multiple write calls. * * @param b * @param off * @param len * @throws IOException * @throws FileEncryptionException * @throws FileIntegrityException * @throws ShortBufferException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws RandomDataGenerationException */ public synchronized void write(byte[] b, int off, int len) throws IOException, FileEncryptionException, FileIntegrityException { if (!exists() || realLength() == 0) { throw new FileEncryptionException( "File has not been initialized properly!"); } try { if (writeRecursionProtector > 1) { throw new FileEncryptionException( "write recursion level exceeded!"); } else { writeRecursionProtector++; } if ((b == null) || ((b.length - off) < len)) throw new IOException("buffer null or empty"); long curpos = getFilePointer(); // offset within chunk int offset = (int) currentchunkoffset(); // check preconditions // 1. does buffer span multiple chunks? boolean singlechunk = (len <= (CHUNK_DATA_SIZE - offset)); if (singlechunk) { byte[] tmp = Arrays.copyOfRange(b, off, off + len); // initial write call at beginning of the file needs extra care // as // we have no preceding chunks AND are in the last chunk at the // same // time if ((length() == 0) && (curpos == 0)) { // no need to decrypt anything as we're at the beginning .. writeLastChunk(tmp, 0); skipBytes(len); writeRecursionProtector--; return; } if (currentchunkpointer() < lastchunkpointer()) { // we're writing within one of the regular chunks if ((len == CHUNK_DATA_SIZE) && (offset == 0)) { // optimization - if the whole chunk is written at once, // no // data need to be merged writeChunk(tmp, currentchunkpointer()); skipBytes(len); } else { // chunk data needs to be merged byte[] decChunk = readChunk(currentchunkpointer()); if (offset >= decChunk.length) { throw new FileEncryptionException( "read() offset mismatch in decrypted data!"); } // merge & write chunk data. System.arraycopy(b, off, decChunk, offset, len); writeChunk(decChunk, currentchunkpointer()); skipBytes(len); } } else if (currentchunkpointer() == lastchunkpointer()) { // we're starting to write within the current last chunk of // this // file; we are NOT just about to start a new last chunk byte[] decChunk = readLastChunk(currentchunkpointer()); // merge chunk data. a little bit more complex as wee may // need // to merge a partial chunk byte[] merged = new byte[Math.max(decChunk.length, offset + len)]; System.arraycopy(decChunk, 0, merged, 0, decChunk.length); System.arraycopy(b, off, merged, offset, len); writeLastChunk(merged, currentchunkpointer()); skipBytes(len); } else { // we're writing beyond the former last chunk of this file. // this // may happen // 1. by seeking beyond the former length of this file, // 2. when just having completely filled the former last // chunk long idx_newlastchunk = currentchunkpointer(); long idx_oldlastchunk = lastchunkpointer(); // number of chunks between old and new last chunk, e.g. in // case // of seek() long diff = idx_newlastchunk - idx_oldlastchunk - 1; // reencrypt old last chunk byte[] decChunk = readLastChunk(idx_oldlastchunk); byte[] chunk = new byte[CHUNK_DATA_SIZE]; System.arraycopy(decChunk, 0, chunk, 0, decChunk.length); // fill remaining space with null bytes Arrays.fill(chunk, decChunk.length, CHUNK_DATA_SIZE, (byte) 0x00); writeChunk(chunk, idx_oldlastchunk); skipBytes(CHUNK_DATA_SIZE); // write intermediate chunks, if there are any (e.g. seek // into // file) for (long i = diff; i > 0; i--) { Arrays.fill(chunk, (byte) 0x00); writeChunk(chunk, idx_newlastchunk - i); skipBytes(CHUNK_DATA_SIZE); } // now write new last chunk byte[] lastchunk = new byte[offset + len]; Arrays.fill(lastchunk, 0, offset, (byte) 0x00); System.arraycopy(b, off, lastchunk, offset, len); writeLastChunk(lastchunk, idx_newlastchunk); skipBytes(lastchunk.length); } writeRecursionProtector--; } else { // we need to write in more than one chunk // strategy: split array into preceding bytes, a number of // chunk-sized pieces and remaining bytes. write these parts // separately and recursively; NOTE: max recursion level should // be 1 // calculate partial chunk at beginning int preceding = 0; if (offset != 0) { preceding = CHUNK_DATA_SIZE - offset; if (preceding > 0) { write(b, off, preceding); } } // we are now aligned with the current chunk offset // calculate number of chunks to be written int nchunks = (len - preceding) / CHUNK_DATA_SIZE; // write chunk-sized pieces for (int i = 0; i < nchunks; i++) { write(b, off + preceding + (i * CHUNK_DATA_SIZE), CHUNK_DATA_SIZE); } // remaining bytes int remainder = (len - preceding) % CHUNK_DATA_SIZE; // write remaining bytes, if there are any if (remainder > 0) { write(b, off + preceding + (nchunks * CHUNK_DATA_SIZE), remainder); } writeRecursionProtector--; } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | ShortBufferException | RandomDataGenerationException e) { throw new FileEncryptionException("Error during write()", e); } } /** * returns the offset of an encrypted chunk for the given chunk index, * taking into account any preceding metadata within the file * * @param index * @return offset at which the given <b>encrypted </b> chunk starts * @throws IOException */ protected synchronized long chunkOffset(long index) throws IOException { // convert to long *prior to* multiplication to avoid // overflow return ((long) fHeader.headerSize()) + ((long) CHUNK_ENC_SIZE * (long) index); } /** * returns number of chunks * * @return * @throws IOException */ protected synchronized long numchunks() throws IOException { if (backingRandomAccessFile.length() <= fHeader.headerSize()) { return 0; } else { long len = backingRandomAccessFile.length() - fHeader.headerSize(); // if file length is aligned with chunk size, we have exactly (len / // chunksize) chunks; otherwise also add the last partial chunk return (((len % CHUNK_ENC_SIZE) == 0) ? (len / CHUNK_ENC_SIZE) : ((len / CHUNK_ENC_SIZE) + 1)); } } /** * indicates to which chunk the file pointer currently points; the first * chunk has the index 0 * * @return current chunk index, or -1, if fp points to header * @throws IOException */ protected synchronized long currentchunkpointer() throws IOException { long fp = backingRandomAccessFile.getFilePointer(); if (fp < fHeader.headerSize()) { return -1; } else { fp -= fHeader.headerSize(); return (fp / CHUNK_ENC_SIZE); } } /** * indicates to which data offset within the current chunk the file pointer * currently points * * @return * @throws IOException */ protected synchronized int currentchunkoffset() throws IOException { long fp = backingRandomAccessFile.getFilePointer(); if (fp < fHeader.headerSize()) { return -1; } else { fp -= fHeader.headerSize(); if (fp == 0) { return 0; } else { return (int) ((fp % CHUNK_ENC_SIZE) - CHUNK_IV_SIZE); } } } /** * always points to the respective last chunk * * @return * @throws IOException */ protected synchronized long lastchunkpointer() throws IOException { long len = backingRandomAccessFile.length(); if (len < fHeader.headerSize()) { return -1; } else { len -= fHeader.headerSize(); int offset = (int) (len % CHUNK_ENC_SIZE); if ((offset == 0) && (len == 0)) { // at the very beginning we're in chunk 0 return 0; } else if ((len != 0) && (offset == 0)) { // right at the offset of a new chunk, we still have to return // the old chunk return ((len / CHUNK_ENC_SIZE) - 1); } else { return (len / CHUNK_ENC_SIZE); } } } /** * @param n * @return * @throws IOException * @throws FileEncryptionException * @see java.io.RandomAccessFile#skipBytes(int) */ public synchronized int skipBytes(int n) throws IOException, FileEncryptionException { long pos, len, newpos; if (n <= 0) { return 0; } pos = getFilePointer(); len = length(); newpos = pos + n; if (newpos > len) { newpos = len; } seek(newpos); return (int) (newpos - pos); } /** * Method encapsulates {@link java.io.RandomAccessFile#getFilePointer()} and * adds additional logic for omitting chunk meta data. File pointer offsets * are converted to corresponding positions within the encrypted * {@link RandomAccessFile} * * @return virtual plantext position within this file * @throws IOException * @see java.io.RandomAccessFile#getFilePointer() */ public long getFilePointer() throws IOException { long realFP = backingRandomAccessFile.getFilePointer(); if (realFP < fHeader.headerSize()) { return -1; } else { realFP -= fHeader.headerSize(); if (realFP == 0) { return 0; } else { // NOTE: We rely on the assumption the real file pointer never // points into any metadata structure. Any method which // manipulates the file pointer has to take this into account. long virtfp; long virtidx; int virtoffset; virtidx = (realFP / CHUNK_ENC_SIZE); virtoffset = (int) (realFP % CHUNK_ENC_SIZE - CHUNK_IV_SIZE); virtfp = virtidx * CHUNK_DATA_SIZE + virtoffset; return virtfp; } } } /** * Method encapsulates {@link java.io.RandomAccessFile#seek(long)} and adds * additional logic for omitting chunk meta data. File pointer offsets are * converted to corresponding positions within the encrypted * {@link RandomAccessFile}. * * @param pos * virtual plaintext position within this file * @throws IOException * @throws FileEncryptionException * @see java.io.RandomAccessFile#seek(long) */ public synchronized void seek(long pos) throws IOException, FileEncryptionException { long virtidx = (pos / CHUNK_DATA_SIZE); int virtoffset = (int) (pos % CHUNK_DATA_SIZE); // determine corresponding position w.r.t. chunk metadata; NOTE: we have // to make sure the file pointer never points into crypto metadata // structures long realPos = virtidx * CHUNK_ENC_SIZE + CHUNK_IV_SIZE + virtoffset + fHeader.headerSize(); if (isFPValid(realPos)) { backingRandomAccessFile.seek(realPos); } else { throw new FileEncryptionException( "FP may not point into crypto metadata!"); } } /** * checks if given file pointer points to a crypto meta data structure * * @return <code>true</code> if fp points into crypto meta data (header, IV, * authentication tag), <code>false</code> otherwise * @throws IOException * @throws FileEncryptionException * if fp currently points into an initialization vector or into * the authentication tag */ protected boolean isFPValid(long newpos) throws IOException { if (newpos < fHeader.headerSize()) { return false; } else { newpos -= fHeader.headerSize(); } long offset = newpos % CHUNK_ENC_SIZE; if (offset < CHUNK_IV_SIZE) { // fp currently points into IV data strcuture // throw new FileEncryptionException( // "FP points into IV data structure!"); return false; } if (offset >= CHUNK_IV_SIZE + CHUNK_DATA_SIZE) { // fp currently points into authentication tag data structure // throw new FileEncryptionException( // "FP points into authentication tag!"); return false; } return true; } /** * @return * @throws IOException * @see java.io.RandomAccessFile#length() */ public synchronized long length() throws IOException { if (realLength() == 0) { return 0; } else { long metadatasize = (numchunks() * (CHUNK_IV_SIZE + CHUNK_TLEN)) + fHeader.headerSize(); return realLength() - metadatasize; } } /** * @return * @see java.io.File#getAbsolutePath() */ public String getAbsolutePath() { return backingFile.getAbsolutePath(); } /** * returns the real file length including all meta data * * @return * @throws IOException */ public long realLength() throws IOException { long ret = backingRandomAccessFile.length(); return ret; } /** * Sets this encrypted file's length (@see * java.io.RandomAccessFile#setLength(long)). File truncation/extension is * specifically handled by re-encrypting this file's last full and partial * chunk * * @param newLength * @throws IOException * @throws FileEncryptionException * @throws FileIntegrityException * @throws ShortBufferException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidAlgorithmParameterException * @throws InvalidKeyException * @throws RandomDataGenerationException * */ public synchronized void setLength(long newLength) throws IOException, FileEncryptionException, FileIntegrityException { try { long oldlen = length(); int offset_new = (int) (newLength % CHUNK_DATA_SIZE); int offset_old = (int) (oldlen % CHUNK_DATA_SIZE); long idx_newlastchunk; if ((offset_new == 0) && (newLength == 0)) { // at the very beginning we're in chunk 0 idx_newlastchunk = 0; } else if ((newLength != 0) && (offset_new == 0)) { // we are right at the offset of a new chunk idx_newlastchunk = ((newLength / CHUNK_DATA_SIZE) - 1); } else { idx_newlastchunk = (newLength / CHUNK_DATA_SIZE); } long idx_oldlastchunk = lastchunkpointer(); byte[] decChunk; boolean truncate = false; if (newLength > oldlen) { // file will be extended // check if one or more new chunks need to be created if (idx_newlastchunk == idx_oldlastchunk) { // we're operating within the current last chunk of this // file; no new chunks need to be created decChunk = readLastChunk(idx_oldlastchunk); // we write beyond the former last byte in the last chunk byte[] lastChunk = new byte[offset_new]; // copy old chunk System.arraycopy(decChunk, 0, lastChunk, 0, decChunk.length); // contents in extended portion of the file are "undefined", // for // noew we just reset them to 0 Arrays.fill(lastChunk, decChunk.length, offset_old, (byte) 0x00); writeLastChunk(lastChunk, idx_newlastchunk); } else if (idx_newlastchunk > idx_oldlastchunk) { // number of chunks need to be increased long diff = idx_newlastchunk - idx_oldlastchunk - 1; // reencrypt old last chunk decChunk = readLastChunk(idx_oldlastchunk); byte[] chunk = new byte[CHUNK_DATA_SIZE]; System.arraycopy(decChunk, 0, chunk, 0, decChunk.length); // fill remaining space with null bytes Arrays.fill(chunk, decChunk.length, CHUNK_DATA_SIZE, (byte) 0x00); writeChunk(chunk, idx_oldlastchunk); // write intermediate chunks, if there are any (e.g. seek // into // file) for (long i = diff; i > 0; i--) { Arrays.fill(chunk, (byte) 0x00); writeChunk(chunk, idx_newlastchunk - i); skipBytes(CHUNK_DATA_SIZE); } // now write new last chunk byte[] lastchunk = new byte[offset_new]; Arrays.fill(lastchunk, (byte) 0x00); writeLastChunk(lastchunk, idx_newlastchunk); } else { log.error("setLength(): Offset / Size mismatch while setting new length of file!"); } } else if (newLength < oldlen) { // file will be truncated // check if one or more new chunks need to be removed if (idx_newlastchunk == idx_oldlastchunk) { // we're operating within the current last chunk of this // file; reencrypt its data decChunk = readLastChunk(idx_oldlastchunk); byte[] lastChunk = new byte[offset_new]; // truncate old chunk and write it System.arraycopy(decChunk, 0, lastChunk, 0, lastChunk.length); writeLastChunk(lastChunk, idx_newlastchunk); truncate = true; } else if (idx_newlastchunk < idx_oldlastchunk) { // number of chunks need to be decreased if (implementsAuthentication()) { // also remove authentication tags of intermediate // chunks to // be removed for (long i = idx_newlastchunk + 1; i <= idx_oldlastchunk; i++) { authTagVerifier.removeChunkAuthTag(i); } // no need to update the file auth tag at this point, as // this will automatically be handled when writing the // new // last chunk } // reencrypt new last chunk decChunk = readChunk(idx_newlastchunk); byte[] chunk = new byte[offset_new]; System.arraycopy(decChunk, 0, chunk, 0, offset_new); writeLastChunk(chunk, idx_newlastchunk); truncate = true; } else { log.error("setLength(): Offset / Size mismatch while setting new length of file!"); } } else { log.info("setLength(): setLength(): No length adjustment was necessary!"); return; } if (implementsCaching()) { flush(); } // call setLength in case of truncation to remove any obsolete // trailing data. // NOTE: only re-adjust length in case of truncation. if the file is // to be extended, this has happened by now due to write() and // flush() calls. if the size did not change, calling setLength() is // not necessary if (truncate) { // derive real new length from given value long virtidx; int virtoffset; long realLen; virtidx = (newLength / CHUNK_DATA_SIZE); virtoffset = (int) (newLength % CHUNK_DATA_SIZE); // determine corresponding position w.r.t. chunk metadata realLen = virtidx * CHUNK_ENC_SIZE + fHeader.headerSize() + ((virtoffset > 0) ? CHUNK_IV_SIZE + CHUNK_TLEN + virtoffset : 0); // reflect changes to backend file // also, setLength() re-adjusts file pointer in case of // truncation backingRandomAccessFile.setLength(realLen); } } catch (RandomDataGenerationException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException | ShortBufferException e) { throw new FileEncryptionException("Error during setLength()", e); } } /** * indicates if this {@link EncRandomAccessFile}-instance has already been * made persistent or if it only exists in memory (see {@link File#exists()} * . * * @return <code>true</code>, if this file has already been made persistent, * <code>false</code> otherwise */ public boolean exists() { return backingFile.exists(); } // public boolean isWritable() { // return this.writable && backingFile.canWrite(); // } /** * returns the current share key version of this file as stored within the * file header, which has been used for encrypting this files specific file * encryption key * * @return share key version of this file * @throws FileEncryptionException * if share key version has not yet been defined */ public int getShareKeyVersion() throws FileEncryptionException, IOException { if (!isOpen()) { throw new IOException("File has not been opened!"); } else { int ret; if ((fHeader == null) || (ret = fHeader.getShareKeyVersion()) == -1) { throw new FileEncryptionException( "Share key version has not been defined!"); } else { return ret; } } } /** * rebuilds the file authentication tag tree, if necessary, and writes the * new file authentication tag to the header */ protected abstract void flushAuthData() throws FileEncryptionException, IOException; /** * helper class for instance management */ protected static class InstanceEntry { private String normalizedFilename; private boolean writable; /** * @param fileName * @param writable */ private InstanceEntry(File file, boolean writable) { this.normalizedFilename = FilenameUtils.normalize(file .getAbsolutePath()); this.writable = writable; } /** * @return the normalizedFilename */ public String getNormalizedFilename() { return normalizedFilename; } /** * @return the writable */ public boolean isWritable() { return writable; } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((normalizedFilename == null) ? 0 : normalizedFilename .hashCode()); result = prime * result + (writable ? 1231 : 1237); return result; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; InstanceEntry other = (InstanceEntry) obj; if (normalizedFilename == null) { if (other.normalizedFilename != null) return false; } else if (!normalizedFilename.equals(other.normalizedFilename)) return false; if (writable != other.writable) return false; return true; } public static InstanceEntry instance(File backingFile, boolean writeable) { return new InstanceEntry(backingFile, writeable); } } // protected final static Hashtable<InstanceEntry, EncRandomAccessFile> // instanceMap = new Hashtable<InstanceEntry, EncRandomAccessFile>(); // // protected static void registerInstance(EncRandomAccessFile encFile) // throws FileEncryptionException { // // if (getInstance(encFile.getAbsolutePath(), encFile.isWritable()) != null) // { // throw new FileEncryptionException("instance already registered!"); // } else { // instanceMap.put( // InstanceEntry.instance(encFile.getAbsolutePath(), // encFile.isWritable()), encFile); // } // } // // protected static boolean removeInstance(String filename, boolean // writable) { // return (instanceMap.remove(InstanceEntry.instance(filename, writable)) != // null); // } // // protected static EncRandomAccessFile getInstance(String filename, // boolean writable) { // return instanceMap.get(InstanceEntry.instance(filename, writable)); // } /** * Method opens an instance of a {@link EncRandomAccessFile}-implementation * for an already existing file. For creating new files, see * {@link #create(int, SecretKey)}. * * NOTE: After an instance has been opened with this method, it still needs * to be initialized with the corresponding share key from the share * metadata DB. * * @throws FileEncryptionException * @throws IOException * @throws NoSuchProviderException * @throws NoSuchPaddingException * @throws NoSuchAlgorithmException */ public abstract void open() throws IOException, FileEncryptionException; /** * Method creates a new backend file which is to be managed with this * {@link EncRandomAccessFile}-instance with the given arguments (and, * correspondingly, read/write access). A new header is generated and * written. This method assumes there currently exists no file at the * specified location, otherwise an exception is thrown. For opening * existing files, see {@link #open(String)}. * * @param shareKeyVersion * latest share key version * @param shareKey * latest share key * @throws FileEncryptionException * @throws IOException * @throws BadPaddingException * @throws IllegalBlockSizeException * @throws InvalidKeyException * @throws RandomDataGenerationException * @throws NoSuchProviderException * @throws NoSuchPaddingException * @throws NoSuchAlgorithmException */ public abstract void create(int shareKeyVersion, SecretKey shareKey) throws FileEncryptionException, IOException; /** * @throws IOException * @see java.io.RandomAccessFile#close() */ public synchronized void close() throws IOException { if (isOpen()) { // make sure all data have been flushed if (writable && implementsCaching() && cache.needsToBeWritten) { log.warn("close(): Cached data still need to be written, calling flush()"); flush(); } // invalidate cache, ... if (implementsCaching()) this.cache = null; if (implementsAuthentication()) this.authTagVerifier = null; backingRandomAccessFile.close(); setOpen(false); setInitialized(false); } else { log.warn("close(): File not open!"); } } /** * Method creates a new {@link AESCBCRandomAccessFile} with the given * arguments (and, correspondingly, read/write access). This method assumes * there currently exists no file at the specified location, otherwise an * exception is thrown. For opening existing files, see * {@link #open(File, String)}. * * @param shareKeyVersion * latest share key version * @param shareKey * latest share key * @param file * file to create * @return * @throws InvalidKeyException * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws InvalidAlgorithmParameterException * @throws NoSuchProviderException * @throws RandomDataGenerationException * @throws IllegalBlockSizeException * @throws BadPaddingException * @throws FileEncryptionException * @throws IOException */ public static EncRandomAccessFile create(int shareKeyVersion, SecretKey shareKey, File file) throws FileEncryptionException, IOException { // do nothing - to be implemented in subclasses return null; } public static EncRandomAccessFile create(int shareKeyVersion, SecretKey shareKey, String file) throws FileEncryptionException, IOException { // do nothing - to be implemented in subclasses return null; } /** * Method opens a {@link AESCBCRandomAccessFile}-instance with the given * arguments for an already existing file. For creating new files, see * {@link #create(int, SecretKey, File)}. NOTE: After an instance has been * obtained with this method, ist still needs to be initialized with the * corresponding share key from the share metadata DB (see * {@link #initWithShareKey(SecretKey)}). * * @param file * @param writable * @return * @throws FileEncryptionException * @throws IOException * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws NoSuchProviderException * @throws InvalidKeyException * @throws IllegalBlockSizeException * @throws BadPaddingException * @throws InvalidAlgorithmParameterException * @throws RandomDataGenerationException */ public static EncRandomAccessFile open(File file, boolean writable) throws FileEncryptionException, IOException { // do nothing - to be implemented in subclasses return null; } public static EncRandomAccessFile open(String file, boolean writable) throws FileEncryptionException, IOException { // do nothing - to be implemented in subclasses return null; } abstract protected void printInstanceMap(); /** * returns an instance for the given arguments * * @param file * backend file to be written/read * @param writable * <code>true</code> if file should be opened writable, * <code>false</code> otherwise * @return * @throws InvalidKeyException * @throws NoSuchAlgorithmException * @throws NoSuchPaddingException * @throws InvalidAlgorithmParameterException * @throws NoSuchProviderException * @throws RandomDataGenerationException */ public static EncRandomAccessFile getInstance(File file, boolean writable) throws FileEncryptionException, IOException { // do nothing - to be implemented in subclasses return null; } /** * renames and switches the encapsulated {@link File}-instance. * * NOTE: If the given File f to rename to currently exists, contains data * and/or has an open file handle, success of this method may be * platform-dependent. see {@link File#renameTo(File)}. * * @param f * @return <code>true</code> if renaming succeeded, <code>false</code> * otherwise */ abstract public boolean renameTo(File f); // protected static Hashtable getInstanceMap() { // return null; // } /** * Convenience method unifying {@link #seek(long)} and {@link #read(byte[])} * in one synchronized method * * @param fileoffset * @param buf * @return * @throws IOException * @throws FileEncryptionException * @throws FileIntegrityException */ public synchronized int readAt(long fileoffset, byte[] buf) throws IOException, FileEncryptionException, FileIntegrityException { seek(fileoffset); return read(buf); } /** * Convenience method unifying {@link #seek(long)} and * {@link #write(byte[], int, int)} in one synchronized method * * @param fileoffset * @param buf * @param bufferoffset * @param length * @throws IOException * @throws FileEncryptionException * @throws FileIntegrityException */ public synchronized void writeAt(long fileoffset, byte[] buf, int bufferoffset, int length) throws IOException, FileEncryptionException, FileIntegrityException { seek(fileoffset); write(buf, bufferoffset, length); } /** * tries to lock the underlying {@link RandomAccessFile}, if opened * * @param blocking * @return * @throws IOException */ public synchronized FileLock lock(boolean blocking) throws IOException { if (isOpen()) { FileChannel channel = backingRandomAccessFile.getChannel(); FileLock ret; if (blocking) { ret = channel.lock(); } else { ret = channel.tryLock(); } return ret; } else { throw new IOException("Can't acquire lock for closed file!"); } } }